Add shopify store info --store <domain> command#7660
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
|
||
| static descriptionWithMarkdown = `Reads metadata for a store from the Business Platform Destinations and Organizations APIs. | ||
|
|
||
| Tier 1 and Tier 2 fields work without \`store auth\`. Tier 3 fields (shop owner, timezone, features, setup required) require \`store auth\` and are only included when \`--verbose\` is set. |
There was a problem hiding this comment.
Nit: This reads like harness session language and could be clarified.
| -j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON. Automatically disables color output. | ||
| -s, --store=<value> (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to authenticate | ||
| against. | ||
| -s, --store=<value> (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store. |
There was a problem hiding this comment.
We are losing some info here + below. I am debating whether it's better to have a shared flag entirely, or there's value in overriding the description.
shopify store info <store> commandshopify store info --store <domain> command
d67e8d3 to
c9531e3
Compare
38490df to
979bd9a
Compare
979bd9a to
c1e7a69
Compare
Adds a new read-only command that surfaces shop metadata from BP Destinations and BP Organizations APIs, with optional Admin API enrichment via `--verbose`. Supports `--json` output and per-field graceful degradation via a `_field_errors` envelope. Tier 1 + Tier 2 fields work without `store auth` for the shop; Tier 3 fields (shop_owner, timezone, features, setup_required) require `store auth` and are opt-in via `--verbose`. Fixes #22724 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces repeated `if (value) result.field = value` chains with a single `compact()` pass from `@shopify/cli-kit/common/object`, flattening buildResult, applyVerboseFields, buildPlan, and mapAdminShop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns with the rest of the store: namespace (e.g. store execute) which takes the shop domain via `-s, --store` rather than a positional. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoists the `-s, --store` flag definition into packages/store/src/cli/flags.ts
following the themeFlags/appFlags precedent. `store auth`, `store execute`,
and `store info` now share the same flag declaration.
Side effect: the per-command verbal description ("…to authenticate against",
"…to execute against", "…to inspect") collapses into "The myshopify.com
domain of the store." — visible only in --help. The verb is redundant with
the command name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BP's Destinations + Organizations APIs return primaryDomain/webUrl as
full URLs with scheme ("https://shop.myshopify.com"), not bare hosts.
Strict string equality against the requested store FQDN missed every
real shop. They also return `handle: null` and a non-subdomain
`shortName` (e.g. "ACT"), so admin_url construction had nothing to work
with.
- Extract host from URL fields before comparing (new info/host.ts).
- Derive the canonical myshopify subdomain from primaryDomain/webUrl
and overwrite destination.handle with it so admin_url is built from
the actual shop subdomain.
- Use primaryDomain (storefront URL) for primary_url instead of
webUrl/url (both are admin URLs ending in /admin).
- Drop the misleading `url` field from OrganizationShopFields and the
Org GraphQL selection — it's the admin URL, not the storefront.
- Correct features.branding type from boolean to string (it's an enum
like "SHOPIFY", not a flag).
Verified live against ariel-caplan-test: Tier 1, 2, and 3 all populate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keys render as "Store Type" instead of "store_type", and GraphQL enum strings like APP_DEVELOPMENT render as "App Development". Currency codes like USD pass through unchanged.
CLI commands take --store as a domain, so BP/Core shop IDs and the owning-org id have no follow-on use for users of this command. is_main_shop is a billing-contract internal that almost no user cares about. Removed from both text and JSON output; org id is kept as an internal-only type to drive the BP Organizations request.
Local-time output without a timezone is ambiguous when sharing output across regions; rendering in UTC makes timestamps unambiguous regardless of where the user is.
Drop references to "Tier", "Identity", and "Business Platform" from the command description, section titles, and error reasons — those are internal taxonomy that users don't recognize.
--verbose is a global "show your work" flag (more diagnostic output) across the CLI; using it to mean "include more result fields" is a mismatched concept. --full reads naturally as "give me the full set of fields".
Drop the features sub-object: storefront is uniformly true, branding is cosmetic, harmonizedSystemCode is niche cross-border tooling. Lift shopifyPlus to a top-level `plus` boolean — it overlaps with plan.name but is relevant enough on its own to deserve a clean signal rather than requiring callers to regex-match plan names. --full now adds exactly: shop_owner, timezone, setup_required, plus.
…uthed The flag was redundant: we already know whether the store is authenticated by the time we compose the result, so gating the Admin fetch behind an opt-in flag just added friction without unlocking anything. Now `store info` quietly includes shop_owner/timezone/ setup_required/plus when `store auth` exists for the store, and omits them otherwise. The `auth_status: not authenticated` field already serves as the discoverability hint, so we don't pollute output with field errors for fields the user never asked for.
Replace data-source-based section names (Overview / Plan & lifecycle / Admin details) with categories users actually think in: Store, Access, Plan, Activity. Timezone is a store property, so it sits in Store rather than Activity.
The --verbose flag was removed when Admin-sourced fields became automatic for authed shops; the test still asserted on it.
knip flagged a handful of types and option interfaces that were exported but only referenced inside their own file. Also drop the stale --full and Tier-1/2 comments from the result envelope.
Both @Shopify/organizations and the store info plumbing were decoding base64-encoded organization GIDs. Pull the helper into the organizations package's models module and export it, then have store info import it. Returns string | undefined so each caller can decide what to do on failure: fetchOrganizations still aborts (the id is required to display the org); store info silently omits the id (the BP Organizations request would have failed with a garbage id anyway).
The base64↔gid logic isn't organization-specific, so a helper named decodeOrganizationGid in @Shopify/organizations was misleading. Add three generic primitives in @shopify/cli-kit/common/gid: - numericIdFromGid: extract /<digits> from a plain gid://... - numericIdFromEncodedGid: same, but for base64-encoded gids - encodeGid: base64-encode a plain gid Thread these through every site that does the same work: - @Shopify/organizations fetch.ts - @shopify/store store info destinations - app-management-client.ts (numberFromGid/idFromEncodedGid wrappers keep their domain semantics — auto-detect numeric strings, fail-fast on bad gids — but delegate the regex/base64 work) Domain-validated extractors like extractBulkOperationId (@shopify/app bulk operations) stay; they require a specific gid prefix that the generic helper doesn't enforce. Drops the store→organizations dependency that existed only for this helper.
Neither helper was specific to store info — extractHost is a generic URL→host parser, and extractMyshopifyHandle is Shopify-domain logic that already has neighbors in cli-kit. Move both into @shopify/cli-kit/common/url alongside isValidURL and safeParseURL, add direct tests, and delete the store/info/host.ts module. extractHost now uses safeParseURL to avoid a bare try/catch.
Follow the repo convention used by other packages: queries live in .graphql files under src/cli/api/graphql/<api>/queries/, with typed documents generated alongside. Replaces hand-written inline query strings and hand-typed response interfaces with the businessPlatform- RequestDoc / businessPlatformOrganizationsRequestDoc / adminRequestDoc typed-document variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The schema files are sourced from external repos at codegen time (mirroring how packages/app and packages/organizations work) and shouldn't be tracked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align `store info` to the store-management project brief's dev-store
contract, sourcing every field from the Business Platform: the result is
now camelCase (id, displayName, subdomain, organizationId,
organizationName, storeOwner{name,email}, type, featurePreview,
adminUrl) and `id` is a `gid://shopify/Shop/<id>` GID.
Owner and feature preview come from BP Organizations
(Shop.ownerDetails, Shop.developerPreviewHandle), so the command no
longer needs Admin access. This lets us drop the `store auth`/Admin data
tier entirely — its service modules, GraphQL query and codegen project,
and the per-field error plumbing — along with the timezone, setup
status, and Plus-subscription fields it backed.
`plan` is intentionally deferred (see the TODO notes) pending the
internal-name -> public-handle mapping decision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the store's plan as a public handle (basic | grow | advanced | plus) in both the text and `--json` output, sourced from BP `Shop.planName`. The raw plan names BP returns are Shopify-internal and differ from the marketing names (e.g. `professional` is Grow, `unlimited` is Advanced), so they're mapped through a hardcoded 1:1 table in `plan.ts`. Both the internal name and the public handle are accepted as keys since the exact form BP returns isn't pinned down by the schema. Plans not in the table are treated as unrecognized and omitted from the output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BP destinations query pulled name/handle/shortName/isAppDevelopment to back display-name/type/admin-url fallbacks, but the contract sources those fields from the Organizations call. Trim the destinations query to publicId (for the owning-org lookup) plus the primaryDomain/webUrl used to match the store, and derive admin_url from the validated `--store` domain. The shop GID is built locally from the numeric ShopifyShopID, so nothing is fetched purely to format the `id`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55cad39 to
77cbde9
Compare
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationspackages/cli-kit/dist/public/common/gid.d.ts/**
* Extracts the trailing numeric id from a plain GraphQL global id like
* `gid://shopify/Product/123`.
*
* @param gid - A plain GraphQL global id string.
* @returns The trailing numeric id, or undefined when the string does not end with `/<digits>`.
*/
export declare function numericIdFromGid(gid: string): string | undefined;
/**
* Decodes a base64-encoded GraphQL global id (for example, the form
* Business Platform APIs return) and returns the trailing numeric id.
*
* @param gid - A base64-encoded GraphQL global id.
* @returns The trailing numeric id, or undefined when the decoded string does not end with `/<digits>`.
*/
export declare function numericIdFromEncodedGid(gid: string): string | undefined;
/**
* Encodes a plain GraphQL global id (`gid://...`) as base64, which is the
* form some Business Platform endpoints require.
*
* @param gid - A plain GraphQL global id string to encode.
* @returns The base64-encoded gid.
*/
export declare function encodeGid(gid: string): string;
Existing type declarationspackages/cli-kit/dist/public/common/url.d.ts@@ -12,4 +12,20 @@ export declare function isValidURL(url: string): boolean;
* @param url - The string to parse into a URL.
* @returns A URL object if the parsing is successful, undefined otherwise.
*/
-export declare function safeParseURL(url: string): URL | undefined;
\ No newline at end of file
+export declare function safeParseURL(url: string): URL | undefined;
+/**
+ * Extracts the lowercased hostname from a URL-shaped string. Tolerates
+ * bare hosts (without a scheme) and inputs that come back from APIs as
+ * either or .
+ *
+ * @param value - A URL or bare host string, possibly null/undefined.
+ * @returns The lowercased hostname, or undefined when the input is empty.
+ */
+export declare function extractHost(value: string | null | undefined): string | undefined;
+/**
+ * Extracts the subdomain handle from a URL or host.
+ *
+ * @param value - A URL or host string, possibly null/undefined.
+ * @returns The myshopify subdomain handle, or undefined when the input isn't a URL.
+ */
+export declare function extractMyshopifyHandle(value: string | null | undefined): string | undefined;
\ No newline at end of file
|

Summary
Adds
shopify store info --store <domain>, which surfaces metadata about a store you have access to. Output is aligned to the store-management project brief's dev-store contract and every field is sourced from the Business Platform (BP Destinations + BP Organizations), so the command works withoutstore auth— no Admin API access required.Fields (camelCase in
--json, labelled in text):id(gid://shopify/Shop/<id>)shopifyShopId(GID built locally)displayNamesubdomain--storeorganizationId,organizationNameorganizationForDestinationstoreOwner { name, email }ownerDetailstype(dev/production/ …)Storeenumplan(basic/grow/advanced/plus)Shop.planName, mapped to a public handlefeaturePreviewdeveloperPreviewHandleadminUrl--storedomain--jsonfor machine-readable output; text output renders a single Store details section.organizationId/organizationName) plussubdomainandadminUrlare still returned.planmaps BP's raw internal plan names (which differ from marketing names, e.g.professional→ Grow,unlimited→ Advanced) to a public handle via a hardcoded 1:1 table; unrecognized plans are omitted.Implementation notes
destinations(search:)→currentUserAccount.organizationForDestination(destinationPublicId:), then BP OrganizationsaccessibleShops(search:)matched on the canonical myshopify domain.publicId(to resolve the owning org) and theprimaryDomain/webUrlused to match the store — every surfaced shop field comes from BP Organizations, and the admin GID / admin URL are derived locally rather than fetched.id/organizationIdGIDs are intentionally not surfaced — the CLI addresses stores by domain.Commits
store auth/Admin data tier (service modules, GraphQL query + codegen project, and per-field error plumbing) along with the timezone / setup-status / Plus fields it backed.planpublic-handle mapping.Quality gates
pnpm vitest run(packages/store) — 168 tests passingpnpm type-checkpnpm lintpnpm refresh-manifests— manifest + README regenerated and committedpnpm build-dev-docs— store info description is not embedded ingenerated_docs_data_v2.json; no changeTest plan
shopify store info --store <store>.myshopify.comreturns the contract fields withoutstore auth--jsonoutput matches the camelCase contract--storesurfaces a clearAbortErrorAbortErrorOut of scope / follow-ups
--storeaccepting a GID or integer — should be a separate CLI-wide change tostoreFlags.store/normalizeStoreFqdnrather than localized toinfo. Interactive org/store prompting (which would make--storeoptional) is likewise deferred.Closes shop/issues-develop#22724
🤖 Generated with Claude Code